import direct.directbase.DirectStart
from panda3d.core import CollisionTraverser,CollisionNode
from panda3d.core import CollisionHandlerQueue,CollisionRay
from panda3d.core import Filename,AmbientLight,DirectionalLight
from panda3d.core import PandaNode,NodePath,Camera,TextNode
from panda3d.core import Vec3,Vec4,BitMask32, VBase3
from direct.gui.OnscreenText import OnscreenText
from direct.actor.Actor import Actor
from direct.showbase.DirectObject import DirectObject
import random, sys, os, math, time
from itertools import cycle
import collections

from itemRelated.bananaDrop import AbstractDrop
from itemRelated.bananaDrop import BananaDrop
from playerRelated.character import PlayerCharacter
from familiarRelated.pandaFamiliar import PandaFamiliar

SPEED = 0.5
V0 = 10
GRAVITY = 10

class World(DirectObject):
    """ Class containing informations and configuration of world in which 
        game 'exists'.
        
        attribs:
        _environ - environment loaded into the scene
        _playerCharacter - object representing player character
        _playerActor - 3D actor of player
        _colTraverser - 
        _playerGroundHandler - 
        _playerMode - mode in which player remains; for now can be None or 'pickUp'
        _interactiveCollisionEntries - list of collisionEntries in certain radius around the player
        _interactiveCollisionEntriesRotator - generator used for cycling objects in the list above
        _playerSphereCollisionHandler - collisionHandler for interactive object (such as drops) around the player character
        
    """
    
    _environ = None
    
    _playerCharacter = None
    _playerActor = None
    
    _jumpStartTime = 0.0
    _isJumping = False
    _jumpStartHeight = 0.0
    
    _playerFamiliar = None
    _playerFamiliarActor = None
    
    _colTraverser = None
    _playerGroundHandler = None
    _familiarGroundHandler = None
    
    _playerMode = None
    _playerTarget = None
    
    _interactiveCollisionEntries = []
    _interactiveCollisionEntriesRotator = None
    _playerSphereCollisionHandler = None
    
    def __init__(self):
        
        self.keyMap = {"left":0, "right":0, "forward":0, "cam-left":0,
            "cam-right":0, "inventory":0}
        
        base.win.setClearColor(Vec4(0,0,0,1))
        
        self._colTraverser                          = CollisionTraverser()
        self._playerGroundHandler                   = CollisionHandlerQueue()
        self._familiarGroundHandler                 = CollisionHandlerQueue()
        
        self._interactiveCollisionEntriesRotator    = cycle(self._interactiveCollisionEntries)
        self._playerSphereCollisionHandler          = CollisionHandlerQueue()

        self.showInstructionsOnScreen()
        self.setUpEnvironment()
        
        self._playerCharacter   = PlayerCharacter(self)
        self._playerFamiliar    = PandaFamiliar(self, self._playerActor.getPos())
        self.setAcceptKeys()
        self.isMoving = False

        
        self.setUpCamera()
        taskMgr.add(self.move,"moveTask")
        
    def getPlayerActor(self):
        return self._playerActor
    
    def getPlayerFamiliarActor(self):
        return self._playerFamiliarActor
    
    def setPlayerActor(self, newActor):
        self._playerActor = newActor
        
    def setPlayerFamiliarActor(self, newActor):
        self._playerFamiliarActor = newActor
        
    def getEnvStartPosition(self):
        return self._environ.find("**/start_point").getPos()
    
    def getColTraverser(self):
        return self._colTraverser
    
    
    #Records the state of the arrow keys
    def setKey(self, key, value):
        self.keyMap[key] = value
        
    def showHideInventory(self):
        if self._playerCharacter.getInventory().getInventoryShown():#if inventory is shown
           
            self._playerCharacter.getInventory().hide()
        else:
            self._playerCharacter.getInventory().show()
    
    # Function to put instructions on the screen.
    def addInstructions(self, pos, msg):
        return OnscreenText(text=msg, style=1, fg=(1,1,1,1),
            pos=(-1.3, pos), align=TextNode.ALeft, scale = .05)

    # Function to put title on the screen.
    def addTitle(self, text):
        return OnscreenText(text=text, style=1, fg=(1,1,1,1),
            pos=(1.3,-0.95), align=TextNode.ARight, scale = .07)
        
    def showInstructionsOnScreen(self):
        # Post the instructions
        self.title = self.addTitle("Panda3D Tutorial: Roaming Ralph (Walking on Uneven Terrain)")
        self.inst1 = self.addInstructions(0.95, "[ESC]: Quit")
        self.inst2 = self.addInstructions(0.90, "[Left Arrow]: Rotate Ralph Left")
        self.inst3 = self.addInstructions(0.85, "[Right Arrow]: Rotate Ralph Right")
        self.inst4 = self.addInstructions(0.80, "[Up Arrow]: Run Ralph Forward")
        self.inst6 = self.addInstructions(0.70, "[A]: Rotate Camera Left")
        self.inst7 = self.addInstructions(0.65, "[S]: Rotate Camera Right")
        self.inst8 = self.addInstructions(0.50, "[I]: Show/hide inventory menu")
        self.inst9 = self.addInstructions(0.45, "[T]: Search an interactive target")
        self.inst10 = self.addInstructions(0.40, "[P]: Pick up selected drop")
        self.inst11 = self.addInstructions(0.35, "[j]: Jump")
        
    def setUpEnvironment(self):

        # Set up the environment
        #
        # This environment model contains collision meshes.  If you look
        # in the egg file, you will see the following:
        #
        #    <Collide> { Polyset keep descend }
        #
        # This tag causes the following mesh to be converted to a collision
        # mesh -- a mesh which is optimized for collision, not rendering.
        # It also keeps the original mesh, so there are now two copies ---
        # one optimized for rendering, one for collisions.  

        self._environ = loader.loadModel("environments/world")
        self._environ.reparentTo(render)
        self._environ.setPos(0,0,0)
        
####### creating two banana drops!
        self.ban = BananaDrop(self, -110, 30, 0)
        self.ban2 = BananaDrop(self, -110.5, 29, 0)

        # Create a floater object.  We use the "floater" as a temporary
        # variable in a variety of calculations.
        
        self.floater = NodePath(PandaNode("floater"))
        self.floater.reparentTo(render)
        
    def setAcceptKeys(self):
        # Accept the control keys for movement and rotation

        self.accept("escape", sys.exit)
        self.accept("arrow_left", self.setKey, ["left",1])
        self.accept("arrow_right", self.setKey, ["right",1])
        self.accept("arrow_up", self.setKey, ["forward",1])
        self.accept("a", self.setKey, ["cam-left",1])
        self.accept("s", self.setKey, ["cam-right",1])
        self.accept("arrow_left-up", self.setKey, ["left",0])
        self.accept("arrow_right-up", self.setKey, ["right",0])
        self.accept("arrow_up-up", self.setKey, ["forward",0])
        self.accept("a-up", self.setKey, ["cam-left",0])
        self.accept("s-up", self.setKey, ["cam-right",0])
        self.accept("i", self.showHideInventory)
        self.accept("t", self.setTarget)
        self.accept("p", self.pickUp)
        self.accept("j", self.jump)
    
    def setUpCamera(self):

        base.disableMouse()
        base.camera.setPos(self._playerActor.getX(),self._playerActor.getY()+10,2)


        self.camGroundRay = CollisionRay()
        self.camGroundRay.setOrigin(0,0,1000)
        self.camGroundRay.setDirection(0,0,-1)
        self.camGroundCol = CollisionNode('camRay')
        self.camGroundCol.addSolid(self.camGroundRay)
        self.camGroundCol.setFromCollideMask(BitMask32.bit(0))
        self.camGroundCol.setIntoCollideMask(BitMask32.allOff())
        self.camGroundColNp = base.camera.attachNewNode(self.camGroundCol)
        self.camGroundHandler = CollisionHandlerQueue()
        self._colTraverser.addCollider(self.camGroundColNp, self.camGroundHandler)
        
        

        # Uncomment this line to see the collision rays
#        self.camGroundColNp.show()
       
        # Uncomment this line to show a visual representation of the 
        # collisions occuring
        self._colTraverser.showCollisions(render)
        
        # Create some lighting
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor(Vec4(.3, .3, .3, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection(Vec3(-5, -5, -5))
        directionalLight.setColor(Vec4(1, 1, 1, 1))
        directionalLight.setSpecularColor(Vec4(1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))
        
    # just my awfully ugly function - it compares coordinates of all the entries in the two lists
    # this way, the list won't be unnecessarily recreated if the order of elements changes
    # it's important while rotating targets
    def chEq(self, collisionEntries1, collisionEntries2):
       if len(collisionEntries1) != len(collisionEntries2):
           return False
       if (collisionEntries1 is None) or (collisionEntries1 == []):
           return False
       
       if (collisionEntries2 is None) or (collisionEntries2 == []):
           return False
       
       list1 = []
       list2 = []
       list1.extend(collisionEntries1)
       list2.extend(collisionEntries2)
       
       tempNP1 = None
       tempNP2 = None
       
       abstractDrop1 = None
       abstractDrop2 = None
       
       for element1 in list1:
           for element2 in list2:
               
               tempNP1 = element1.getIntoNodePath()
               tempNP2 = element2.getIntoNodePath()
               
               if not tempNP1.hasPythonTag("dropObject"):
                   return False
               
               if not tempNP2.hasPythonTag("dropObject"):
                   return False
               
               abstractDrop1 = tempNP1.getPythonTag("dropObject")
               abstractDrop2 = tempNP2.getPythonTag("dropObject")
               
               if abstractDrop1.getX() == abstractDrop2.getX():
                   if abstractDrop1.getY() == abstractDrop2.getY():
                       if abstractDrop1.getZ() == abstractDrop2.getZ():

                           list2.remove(element2)
           
           
       if len(list2) > 0:
            return False
        
       return True
        
        
    # Accepts arrow keys to move either the player or the menu cursor,
    # Also deals with grid checking and collision detection
    def move(self, task):

        base.camera.lookAt(self._playerActor)
        if (self.keyMap["cam-left"]!=0):
            base.camera.setX(base.camera, -30 * globalClock.getDt())
        if (self.keyMap["cam-right"]!=0):
            base.camera.setX(base.camera, +30 * globalClock.getDt())

        playerStartPos = self._playerActor.getPos()
        familiarStartPos = self._playerFamiliarActor.getPos()

        if (self.keyMap["left"]!=0):
            self._playerActor.setH(self._playerActor.getH() + 300 * globalClock.getDt())
        if (self.keyMap["right"]!=0):
            self._playerActor.setH(self._playerActor.getH() - 300 * globalClock.getDt())
        if (self.keyMap["forward"]!=0):
            self._playerActor.setY(self._playerActor, -25 * globalClock.getDt())
            if self._playerFamiliar is not None:
                self._playerFamiliar.setPos(self._playerActor.getPos())
                

#         If _playerActor is moving, loop the run animation.
        # If he is standing still, stop the animation.

        if (self.keyMap["forward"]!=0) or (self.keyMap["left"]!=0) or (self.keyMap["right"]!=0):
            if self.isMoving is False:
                
                self._playerCharacter.moveActor()
                self.isMoving = True
                
                if (self._playerFamiliar is not None) and (self.keyMap["forward"]!=0):
                    
                    self._playerFamiliar.moveActor()
                    self._playerFamiliar.lookAt(self._playerActor)

            else:
                if self._playerFamiliar is not None:
                    self._playerFamiliar.lookAt(self._playerActor)
                
        else:
            if self.isMoving:
                #if the character stops moving, reset his target and mode
                self._playerCharacter.stopActor()
                self.isMoving = False
                self._playerMode = None
#                print "resetting target"
                self._playerTarget = None
                if self._playerFamiliar is not None:
                    self._playerFamiliar.stopActor()

        # If the camera is too far from _playerActor, move it closer.
        # If the camera is too close to _playerActor, move it farther.

        camvec = self._playerActor.getPos() - base.camera.getPos()
        camvec.setZ(0)
        camdist = camvec.length()
        camvec.normalize()
        if (camdist > 10.0):
            base.camera.setPos(base.camera.getPos() + camvec*(camdist-10))
            camdist = 10.0
        if (camdist < 5.0):
            base.camera.setPos(base.camera.getPos() - camvec*(5-camdist))
            camdist = 5.0

        # Now check for collisions.

        self._colTraverser.traverse(render)

        # Adjust _playerActor's Z coordinate.  If _playerActor's ray hit terrain,
        # update his Z. If it hit anything else, or didn't hit anything, put
        # him back where he was last frame.
        
        
#        if self._isJumping:
            # character going up:
            # s = v0*t - (a*t*t)/2, where 'a' is gravity
            
            # been trying to implement but suddenly got lazy :P
#            currTime = time.time() - self._jumpStartTime
#            s = V0 * currTime - (GRAVITY * currTime * currTime)/2.0
#            print 's = ', s
#            
#            v = V0 - GRAVITY * currTime
#            #print 'v = ', v
#            if v <= 0:
#                self._isJumping = False

        entries = []
        for i in range(self._playerGroundHandler.getNumEntries()):
            entry = self._playerGroundHandler.getEntry(i)
            entries.append(entry)
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                     x.getSurfacePoint(render).getZ()))
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"):
            self._playerActor.setZ(entries[0].getSurfacePoint(render).getZ())
            pass
        else:
            self._playerActor.setPos(playerStartPos)
            
        # if the player if in the 'pick up' mode and one of the collision entries isn't a terrain
        if self._playerMode == "pickUp":
            for entry in entries:
                if entry.getIntoNode().getName() != "terrain":
                    self.keyMap['forward'] = 0
                    
                    self.getIntoInventory(entry)
                    self._playerMode = None
                    break
                
                
        # collision entries for the familiar:
        entries = []
        for i in range(self._familiarGroundHandler.getNumEntries()):
            entry = self._familiarGroundHandler.getEntry(i)
            entries.append(entry)
            
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                     x.getSurfacePoint(render).getZ()))
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"):
            self._playerFamiliarActor.setZ(entries[0].getSurfacePoint(render).getZ())
            #print entries[0].getSurfacePoint(render).getZ()
        else:
            pass
            self._playerFamiliarActor.setPos(familiarStartPos)

        # Keep the camera at one foot above the terrain,
        # or two feet above _playerActor, whichever is greater.
        
        entries = []
        for i in range(self.camGroundHandler.getNumEntries()):
            entry = self.camGroundHandler.getEntry(i)
            entries.append(entry)
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                     x.getSurfacePoint(render).getZ()))
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"):
            base.camera.setZ(entries[0].getSurfacePoint(render).getZ()+1.0)
        if (base.camera.getZ() < self._playerActor.getZ() + 2.0):
            base.camera.setZ(self._playerActor.getZ() + 2.0)
            
        
        
        # The camera should look in _playerActor's direction,
        # but it should also try to stay horizontal, so look at
        # a floater which hovers above _playerActor's head.
        
        self.floater.setPos(self._playerActor.getPos())
        self.floater.setZ(self._playerActor.getZ() + 2.0)
        base.camera.lookAt(self.floater)
            
        return task.cont
    
    def setTarget(self):
        
        tempInteractiveCollisionEntries = []
        
        for i in range(self._playerSphereCollisionHandler.getNumEntries()):
            entry = self._playerSphereCollisionHandler.getEntry(i)
            
            if entry.getIntoNodePath().getName() != "terrain":
                tempInteractiveCollisionEntries.append(entry)
        
        #print len(tempInteractiveCollisionEntries)
        if not self.chEq(tempInteractiveCollisionEntries, self._interactiveCollisionEntries):
            #print 'not equal'
            self._interactiveCollisionEntries = []
            self._interactiveCollisionEntries.extend(tempInteractiveCollisionEntries)
            self._interactiveCollisionEntriesRotator = cycle(self._interactiveCollisionEntries)
           
        if len(self._interactiveCollisionEntries) > 0:
            self.selectedNodePath = self._interactiveCollisionEntriesRotator.next().getIntoNodePath()
            
            # make the player actor look directly at the selected NodePath
            self._playerActor.lookAt(self.selectedNodePath)
            self._playerActor.setH(self._playerActor.getH() - 180)
            
            self._playerActor.setP(0)
            self._playerActor.setR(0)
            
            #make the selected node path player's target
            self._playerTarget = self.selectedNodePath
            
    def pickUp(self):
        if self._playerTarget is not None:
        
            #if the player has selected a target which is a drop, he may pick it up
            if self._playerTarget.hasPythonTag("dropObject"):
                self._playerMode = "pickUp"
                self.keyMap['forward'] = 1
        
    def getIntoInventory(self, collisionEntry):
        intoNodePath = collisionEntry.getIntoNodePath()
        
        if intoNodePath.hasPythonTag("dropObject"):

            abstractDrop = intoNodePath.getPythonTag("dropObject")
            self._playerCharacter.getInventory().addToInventory(abstractDrop.getItemName(), abstractDrop.getItemQuantity())
            
    #        in the line below, parentNode is a reference to a model of a drop
            parentNode = intoNodePath.getParent()
            parentNode.detachNode()
            self._playerTarget = None
            
    def jump(self):
        self._jumpStartTime = time.time()
        self._isJumping = True
        self._jumpStartHeight = self._playerActor.getZ()
        
        